﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows;
using System.Collections.ObjectModel;
using System.Windows.Media;
using SmartPaginator.Extensions;

namespace SmartPaginator.Elements
{
    public class TableElement: Border, IBlockElement
    {
        #region Constructor

        protected TableElement(Table source)
        {
            Source = source;
            Cells = new ObservableCollection<CellElement>();
            TableGrid = new Grid();
            CellMatrix = new Dictionary<Point, CellElement>();
            this.Child = TableGrid;
            this.BorderBrush = source.BorderBrush;
            this.BorderThickness = source.BorderThickness;
            this.Background = source.Background;
            this.Margin = BlockHelper.GetAdjustedMargin(null, this, Source.Margin);

            RowBackgrounds = new List<Brush>();
        }

        public TableElement(Table source, IElementFactory factory): this(source)
        {
            if (factory == null)
                throw new ArgumentNullException("factory");


            ImportColumns();

            ImportRows(factory);
        }

        #endregion

        #region Properties

        public Table Source { get; private set; }

        public ObservableCollection<CellElement> Cells { get; private set; }

        protected Grid TableGrid { get; private set; }

        protected Dictionary<Point, CellElement> CellMatrix { get; private set; }

        public int RowCount { get; private set; }

        public List<Brush> RowBackgrounds { get; set; }

        public int ColumnCount { get; private set; }

        #endregion

        #region AddChild Method

        public virtual void AddChildElement(UIElement child)
        {
            var cell = child as CellElement;

            Cells.Add(cell);
            TableGrid.Children.Add(cell);

            // Update the margin according to the cellspacing
            cell.Margin = new Thickness(Source.CellSpacing / 2);

            // Update the cell matrix
            UpdateCellMatrix(cell);
        }

        #endregion

        #region GetEmptyCopy members

        public virtual IBlockElement GetEmptyCopy()
        {
            var result = new TableElement(this.Source);
            foreach(var column in this.TableGrid.ColumnDefinitions)
            {
                var columnCopy = new ColumnDefinition();
                columnCopy.Width = column.Width;
                result.TableGrid.ColumnDefinitions.Add(columnCopy);
            }
            result.ColumnCount = this.ColumnCount;
    
            return result;
        }

        #endregion

        #region Harvest Method

        public virtual HarvestResult Harvest(HarvestingContext context, double remainingHeight, IBlockElement previousElement)
        {
            bool isPartiallyHarvested = context.IsPartiallyHarvested(this);

            // If the table requires a page break, let's make sure we are the first in the document.
            if ((this.Source.BreakPageBefore) && (PageColumnElement.IsDirectChild(this)) && !isPartiallyHarvested)
            {
                if (previousElement != null)
                {
                    return HarvestResult.BreakPage;
                }
            }

            // First, let's adjust the margins
            Thickness adjustedMargin;
            if (!isPartiallyHarvested)
            {
                adjustedMargin = BlockHelper.GetAdjustedMargin(previousElement, this, Source.Margin);
            }
            else
            {
                adjustedMargin = BlockHelper.GetAdjustedMargin(null, this, Source.Margin);
                adjustedMargin.Top = 0;
            }
            remainingHeight -= adjustedMargin.Top;

            // Then, let's adjust the borders
            Thickness adjustedBorder;
            if (!isPartiallyHarvested)
                adjustedBorder = new Thickness(BorderThickness.Left, BorderThickness.Top, BorderThickness.Right, 0);
            else
                adjustedBorder = new Thickness(BorderThickness.Left, 0, BorderThickness.Right, 0);
            remainingHeight -= adjustedBorder.Top;


            var harvestedTable = this.GetEmptyCopy() as TableElement;
            var harvestedCellHeights = new Dictionary<CellElement, double>();
            var harvestedRowHeights = new List<double>();

            // Marks the first row to be harvested in this pass...
            int rowOffset = -1;
            bool isHeaderHarvested = false;
            int headerRowCount = TableExtensions.GetHeaderRowCount(this.Source);
            
            for (int row = 0; row < this.RowCount; row++)
            { 
                // Skip rows that have already been fully harvested
                if (IsFullyHarvested(row, context))
                    continue;

                if (rowOffset == -1)
                    rowOffset = row;

                // Harvest the header
                if (!isHeaderHarvested && (row > 0) && (headerRowCount > 0))
                {
                    rowOffset -= headerRowCount;

                    // The header has already been harvested in the original context. By creating a new context, we can harvest it again
                    var context2 = new HarvestingContext();

                    for (int j = 0; j < headerRowCount; j++)
                    {
                        remainingHeight = HarvestRow(j, 0, harvestedTable, context2, remainingHeight, harvestedCellHeights, harvestedRowHeights);
                    }
                }
                isHeaderHarvested = true;

                // See if the next row can fit
                if (CheckIfRowFits(row, remainingHeight, context))
                {
                    remainingHeight = HarvestRow(row, rowOffset, harvestedTable, context, remainingHeight, harvestedCellHeights, harvestedRowHeights);
                }
                else
                {
                    break;
                }
            }

            harvestedTable.RowCount = harvestedTable.TableGrid.RowDefinitions.Count;


            bool breakColumn = false;

            // If finished with the table, apply bottom border and mark as fully harvested
            if (IsFullyHarvested(this.RowCount - 1, context))
            {
                adjustedBorder.Bottom = Source.BorderThickness.Bottom;
                context.MarkAsFullyHarvested(this);
            }
            else
            { 
                // Remove any bottom margin, as we haven't yet finished with the table
                adjustedMargin.Bottom = 0;
                breakColumn = true; // Need to break column, because we coulnd't fit the entire table
            }
            harvestedTable.BorderThickness = adjustedBorder;
            harvestedTable.Margin = adjustedMargin;


            // Copy the appropriate row backgrounds
            if (rowOffset != -1)
            {
                // Copy the background of the header rows
                if (rowOffset != 0)
                {
                    for (int j = 0; j < headerRowCount; j++)
                    {
                        harvestedTable.RowBackgrounds.Add(this.RowBackgrounds[j]);
                    }
                    rowOffset += headerRowCount;
                }

                // Copy the background of the other rows
                int otherRowsCount = harvestedTable.RowCount - harvestedTable.RowBackgrounds.Count;
                for (int i = 0; i < otherRowsCount; i++)
                {
                    harvestedTable.RowBackgrounds.Add(this.RowBackgrounds[i + rowOffset]);
                }
            }

            // Draw the backgrounds
            harvestedTable.DrawBackground();
            return new HarvestResult(harvestedTable, remainingHeight, breakColumn, false);
        }

        public double HarvestRow(int row, int rowOffset, TableElement table, HarvestingContext context, double remainingHeight, 
            Dictionary<CellElement, double> harvestedCellHeights, List<double> harvestedRowHeights)
        {
            double thisRowHeight = 0.0;

            // It fits, so harvest each cell for this row 
            for (int column = 0; column < this.ColumnCount; column++)
            {
                var cell = GetCellAt(column, row);
                if (cell == null)
                    continue;

                if (!harvestedCellHeights.ContainsKey(cell))
                {
                    var harvestResult = cell.Harvest(context, remainingHeight, null);
                    double cellHeight = remainingHeight - harvestResult.RemainingHeight;
                    harvestedCellHeights.Add(cell, cellHeight);

                    var copyElement = harvestResult.HarvestedElement as CellElement;
                    // Update the starting row and row span (consider the cell might be drawn on another page)
                    int newStartingRow = row - rowOffset;
                    int newRowSpan = Grid.GetRowSpan(cell) + Grid.GetRow(cell) - row;
                    Grid.SetRow(copyElement, newStartingRow);
                    Grid.SetRowSpan(copyElement, newRowSpan);
                    table.AddChildElement(copyElement);
                }

                // If this is the last row for the cell, we need to update the row height
                int cellRowSpan = Grid.GetRowSpan(cell);
                int cellRow = Grid.GetRow(cell);
                if (cellRow + cellRowSpan - 1 == row)
                {
                    double thisCellHeight = harvestedCellHeights[cell];
                    int index = 1;
                    while (index < cellRowSpan)
                    {
                        if (harvestedRowHeights.Count >= index)
                        {
                            thisCellHeight -= harvestedRowHeights[harvestedRowHeights.Count - index];
                        }
                        index++;
                    }

                    if (thisCellHeight > thisRowHeight)
                        thisRowHeight = thisCellHeight;
                }
            }
            thisRowHeight += Source.CellSpacing;

            harvestedRowHeights.Add(thisRowHeight);
            return remainingHeight - thisRowHeight;
        }

        private bool IsFullyHarvested(int row, HarvestingContext context)
        {
            for (int column = 0; column < this.ColumnCount; column++)
            {
                var cell = GetCellAt(column, row);
                if (!context.IsFullyHarvested(cell))
                    return false;
            }
            return true;
        }

        private bool CheckIfRowFits(int row, double remainingHeight, HarvestingContext context)
        {
            for (int column = 0; column < this.ColumnCount; column++)
            {
                var cell = GetCellAt(column, row);
                if (cell == null)
                    continue;

                // We don't consider minimum height requirements for cells that started in a previous row. 
                // It will either have been harvested already, or we just broke the page so we must 
                // harvest it anyway
                CellElement previousCell = null;
                if (row > 0)
                    previousCell = GetCellAt(column, row - 1);
                if (cell == previousCell)
                    continue;

                // We don't consider the minimum height for cells that have already started to be harvested,
                // as 
                if (context.HasStartedHarvesting(cell))
                    continue;


                double minCellHeight = cell.CalculateMinHeight();
                if (minCellHeight > remainingHeight)
                    return false; // Well, the remaining space we have in this column doesn't fit our row
            }

            return true;
        }
        
        #endregion

        #region Margin Calculation

        public virtual double CalculateBottomMargin()
        {
            if (double.IsNaN(Source.Margin.Bottom))
                return Source.GetLineHeight();

            return this.Margin.Bottom;
        }

        public virtual double CalculateTopMargin()
        {
            if (double.IsNaN(Source.Margin.Top))
                return Source.GetLineHeight();

            return this.Margin.Top;
        }

        #endregion

        #region CalculateHeight Methods

        public double CalculateHeight()
        {
            if (!double.IsNaN(Height))
                return Height;

            return CalculateContentHeight();
        }

        public double CalculateContentHeight()
        {
            double result = 0;

            //foreach (var child in Items)
            //{
            //    var element = child as FrameworkElement;
            //    if (element != null)
            //    {
            //        result += element.Margin.Top + element.ActualHeight;
            //    }
            //}

            return result;
        }

        public virtual double CalculateMinHeight()
        {
            //if (Items.Count > 0)
            //{
            //    var block = Items[0] as IBlockElement;
            //    if (block != null)
            //        return block.CalculateMinHeight();
            //}

            return 0;
        }

        #endregion

        #region ImportColumns and ImportRows Methods

        protected void ImportColumns()
        { 
            TableGrid.ColumnDefinitions.Clear();

            bool hasExpandingColumn = false;
            foreach (var column in Source.Columns)
            {
                var definition = new ColumnDefinition();
                definition.Width = column.Width;
                if (column.Width.GridUnitType == GridUnitType.Star)
                    hasExpandingColumn = true;
                TableGrid.ColumnDefinitions.Add(definition);
            }

            this.ColumnCount = Source.Columns.Count;

            // If no expanding column was defined, change all Auto columns to single *
            if (!hasExpandingColumn)
            {
                foreach (var column in TableGrid.ColumnDefinitions)
                {
                    if (column.Width == GridLength.Auto)
                        column.Width = new GridLength(1, GridUnitType.Star);
                }
            }
        }

        protected void ImportRows(IElementFactory factory)
        {
            TableGrid.RowDefinitions.Clear();
            int rowIndex = 0;

            foreach (var rowGroup in Source.RowGroups)
            {
                foreach (var row in rowGroup.Rows)
                {
                    int columnIndex = 0;
                    foreach (var cell in row.Cells)
                    {
                        var cellElement = GetCellAt(columnIndex, rowIndex);
                        while (cellElement != null)
                        {
                            columnIndex++;
                            cellElement = GetCellAt(columnIndex, rowIndex);
                        }

                        if (columnIndex >= Source.Columns.Count)
                            throw new ArgumentException("The table defines more cells that can fit on row " + rowIndex.ToString());
                            
                        cellElement = new CellElement(cell, factory);
                        Grid.SetRow(cellElement, rowIndex);
                        Grid.SetColumn(cellElement, columnIndex);
                        AddChildElement(cellElement);
                    }

                    if (row.Background != null)
                        RowBackgrounds.Add(row.Background);
                    else
                        RowBackgrounds.Add(rowGroup.Background);

                    rowIndex++;
                }
            }
        }

        #endregion

        #region Cell Methods

        public CellElement GetCellAt(int column, int row)
        {
            var point = new Point(column, row);
            if (!CellMatrix.ContainsKey(point))
                return null;

            return CellMatrix[point];
        }

        private void UpdateCellMatrix(CellElement cellElement)
        {
            int row = Grid.GetRow(cellElement);
            int rowSpan = Grid.GetRowSpan(cellElement);

            if (TableGrid.RowDefinitions.Count <= row)
            {
                var definition = new RowDefinition();
                definition.Height = GridLength.Auto;
                TableGrid.RowDefinitions.Add(definition);
            }
            
            while (rowSpan > 0)
            {
                int column = Grid.GetColumn(cellElement);
                int columnSpan = Grid.GetColumnSpan(cellElement);

                while (columnSpan > 0)
                {
                    var point = new Point(column, row);
                    if (CellMatrix.ContainsKey(point))
                        throw new InvalidOperationException(String.Format("Table already contains a cell at ({0}, {1})", column, row));

                    CellMatrix[point] = cellElement;

                    columnSpan--;
                    column++;
                }

                rowSpan--;
                row++;
            }

            if (this.RowCount < row)
                this.RowCount = row;
        }


        #endregion

        #region DrawBackgrounds Method

        protected void DrawBackground()
        {
            // Draw row backgrounds
            for (int row = 0; row < RowBackgrounds.Count; row++)
            {
                var brush = RowBackgrounds[row];
                if (brush != null)
                {
                    var border = new Border();
                    border.Background = brush;
                    Grid.SetRow(border, row);
                    Grid.SetColumnSpan(border, TableGrid.ColumnDefinitions.Count);
                    TableGrid.Children.Insert(0, border);
                }
            }

            // Draw column backgrounds
            for (int column = 0; column < Source.Columns.Count; column++)
            {
                var tableColumn = Source.Columns[column];
                if (tableColumn.Background != null)
                {
                    var border = new Border();
                    border.Background = tableColumn.Background;
                    Grid.SetColumn(border, column);
                    Grid.SetRowSpan(border, TableGrid.RowDefinitions.Count);
                    TableGrid.Children.Insert(0, border);
                }
            }
        }

        #endregion

        #region Measure and Arrange Overrides

        protected override Size MeasureOverride(Size constraint)
        {
            var result = base.MeasureOverride(constraint);
            return result;
        }

        protected override Size ArrangeOverride(Size arrangeBounds)
        {
            var result = base.ArrangeOverride(arrangeBounds);
            return result;
        }

        #endregion
    }
}
